package tools;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Line2D;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import math.Point;
import saves.items.Condicao;
import saves.items.Consumivel;
import saves.items.Efeito;
import saves.items.Equipamento;
import saves.items.Item;

/**
 *
 * @author Luan
 */
public class ClassDiagramGenerator extends JFrame {
    private static final long serialVersionUID = 1L;
    public static final boolean ADD_PRIVATE_METHODS = true, ADD_FINALS = false;
    
    private ArrayList<Classe> classes;
    private ArrayList<Conexao> cons;
    private int selection, selCon;
    
    //new conexao
    private int iniMem;
    private boolean iniBeg;

    public static void main(String[] args) {
        new ClassDiagramGenerator();
    }

    public ClassDiagramGenerator() {
        super("ClassDiagramGenerator");

        selection = iniMem = selCon = -1;
        classes = new ArrayList<>();
        cons = new ArrayList<>();
        classes.add(new Classe(Item.class));
        classes.add(new Classe(Equipamento.class));
        classes.add(new Classe(Consumivel.class));
        classes.add(new Classe(Efeito.class));
        classes.add(new Classe(Condicao.class));

        this.setLayout(new BorderLayout());
        this.add(new Desenho(), BorderLayout.CENTER);
        JPanel pnlBotoes = new JPanel();
        JButton btnAdd = new JButton("Add class");
        btnAdd.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String t = JOptionPane.showInputDialog(ClassDiagramGenerator.this, "Write the full name of the class you want to add.", "Add class", JOptionPane.QUESTION_MESSAGE);
                if (t != null && !t.isEmpty())
                    try {
                        classes.add(new Classe(Class.forName(t)));
                        repaint();
                    } catch (ClassNotFoundException ex) {
                        JOptionPane.showMessageDialog(ClassDiagramGenerator.this, "The class name specified does not exist.", "Error", JOptionPane.ERROR_MESSAGE);
                    }
            }
        });
        pnlBotoes.add(btnAdd);
        JButton btnSave = new JButton("Save");
        btnSave.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String t = JOptionPane.showInputDialog(ClassDiagramGenerator.this, "Where would you like to save?", "Save", JOptionPane.QUESTION_MESSAGE);
                if (t != null && !t.isEmpty())
                    try (ObjectOutputStream p = new ObjectOutputStream(new FileOutputStream(new File(t)))) {
                        p.writeObject(classes);
                        p.writeObject(cons);
                    } catch (IOException ex) {
                        JOptionPane.showMessageDialog(ClassDiagramGenerator.this, "There was an error while saving the file.", "Error", JOptionPane.ERROR_MESSAGE);
                    }
            }
        });
        pnlBotoes.add(btnSave);
        JButton btnLoad = new JButton("Load");
        btnLoad.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String t = JOptionPane.showInputDialog(ClassDiagramGenerator.this, "What file would you like to load?", "Load", JOptionPane.QUESTION_MESSAGE);
                if (t != null && !t.isEmpty())
                    try (ObjectInputStream p = new ObjectInputStream(new FileInputStream(new File(t)))) {
                        classes = (ArrayList<Classe>) p.readObject();
                        cons = (ArrayList<Conexao>) p.readObject();
                        repaint();
                    } catch (ClassNotFoundException | IOException ex) {
                        JOptionPane.showMessageDialog(ClassDiagramGenerator.this, "There was an error while loading the file.", "Error", JOptionPane.ERROR_MESSAGE);
                    }
            }
        });
        pnlBotoes.add(btnLoad);
        this.add(pnlBotoes, BorderLayout.SOUTH);

        this.setMinimumSize(new Dimension(600, 400));
        this.pack();
        this.setExtendedState(MAXIMIZED_BOTH);

        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.setLocationRelativeTo(null);
        this.setVisible(true);
    }

    private boolean selectAt(int x, int y) {
        for (int i = 0; i < classes.size(); i++)
            if (new Rectangle(classes.get(i).x, classes.get(i).y, Classe.WIDTH, Classe.LINE_HEIGHT).contains(x, y)) {
                selection = i;
                return true;
            } else if (new Rectangle(classes.get(i).x, classes.get(i).y, Classe.WIDTH, classes.get(i).getHeight()).contains(x, y))
                classes.get(i).removeAt(y - classes.get(i).y);
        return false;
    }
    
    private static class Conexao implements Serializable {
        private static final long serialVersionUID = 1L;
        private int iniClass, iniMember;
        private int endClass, endMember;
        
        private boolean iniBeginning, endBeginning;
        
        private ArrayList<Point.Inteiro> points;

        public Conexao(int iniClass, int iniMember, boolean ib, int endClass, int endMember, boolean eb) {
            this.iniClass = iniClass;
            this.iniMember = iniMember;
            this.endClass = endClass;
            this.endMember = endMember;
            
            this.iniBeginning = ib;
            this.endBeginning = eb;
            
            points = new ArrayList<>();
        }
        
        public void addPoint(Point.Inteiro p) {
            points.add(p);
        }
        
        public boolean removeAt(int x, int y) {
            for (int i = 0; i < points.size(); i++)
                if (points.get(i).equals(x, y, 5)) {
                    points.remove(i);
                    return true;
                }
            return false;
        }
        
        private boolean lineContains(int x1, int y1, int x2, int y2, int x, int y, int r) {
            return new Line2D.Float(x1, y1, x2, y2).intersects(x - r/2, y - r/2, r, r);
        }
        
        public boolean contains(int x, int y, ArrayList<Classe> classes) {
            Point.Inteiro ini = new Point.Inteiro(classes.get(iniClass).getX() + (iniBeginning ? 0 : Classe.WIDTH), classes.get(iniClass).getY() + Classe.LINE_HEIGHT*(iniMember + 1) + Classe.LINE_HEIGHT/2),
                          end = new Point.Inteiro(classes.get(endClass).getX() + (endBeginning ? 0 : Classe.WIDTH), classes.get(endClass).getY() + Classe.LINE_HEIGHT*(endMember + 1) + Classe.LINE_HEIGHT/2);
            int k = 7;
            if (points.isEmpty()) {
                if (lineContains(ini.x, ini.y, end.x, end.y, x, y, k))
                    return true;
            } else {
                if (lineContains(ini.x, ini.y, points.get(0).x, points.get(0).y, x, y, k))
                    return true;
                int i;
                for (i = 1; i < points.size(); i++)
                    if (lineContains(points.get(i - 1).x, points.get(i - 1).y, points.get(i).x, points.get(i).y, x, y, k))
                        return true;
                if (lineContains(points.get(i - 1).x, points.get(i - 1).y, end.x, end.y, x, y, k))
                    return true;
            }
            return false;
        }
        
        public void draw(Graphics g, ArrayList<Classe> classes, boolean sel) {
            g.setColor(sel ? Color.PINK : Color.BLACK);
            Point.Inteiro ini = new Point.Inteiro(classes.get(iniClass).getX() + (iniBeginning ? 0 : Classe.WIDTH), classes.get(iniClass).getY() + Classe.LINE_HEIGHT*(iniMember + 1) + Classe.LINE_HEIGHT/2),
                          end = new Point.Inteiro(classes.get(endClass).getX() + (endBeginning ? 0 : Classe.WIDTH), classes.get(endClass).getY() + Classe.LINE_HEIGHT*(endMember + 1) + Classe.LINE_HEIGHT/2);
            if (points.isEmpty())
                g.drawLine(ini.x, ini.y, end.x, end.y);
            else {
                g.drawLine(ini.x, ini.y, points.get(0).x, points.get(0).y);
                int i;
                for (i = 1; i < points.size(); i++)
                    g.drawLine(points.get(i - 1).x, points.get(i - 1).y, points.get(i).x, points.get(i).y);
                g.drawLine(points.get(i - 1).x, points.get(i - 1).y, end.x, end.y);
            }
        }
    }

    private static class Classe implements Serializable {
        private static final long serialVersionUID = 1L;

        private ArrayList<Membro> atributos, metodos;
        private Class classe;
        private int x, y;
        public static final int WIDTH = 250, LINE_HEIGHT = 20;
        public static final Color FUNDO = new Color(255, 255, 205);

        public Classe(Class c) {
            this.classe = c;
            this.x = this.y = 10;

            atributos = new ArrayList<>();
            Field[] fs = c.getDeclaredFields();
            for (int i = 0; i < fs.length; i++)
                if (!Modifier.isFinal(fs[i].getModifiers()) || ADD_FINALS)
                    atributos.add(new Membro(fs[i].getName(), fs[i].getType(), getVisibilidade(fs[i].getModifiers())));

            metodos = new ArrayList<>();
            Method[] ms = c.getDeclaredMethods();
            for (int i = 0; i < ms.length; i++)
                if (Modifier.isPublic(ms[i].getModifiers()) || ADD_PRIVATE_METHODS)
                    metodos.add(new Membro(getName(ms[i]), ms[i].getReturnType(), getVisibilidade(ms[i].getModifiers())));
        }

        public static String getName(Method m) {
            Class[] ps = m.getParameterTypes();
            if (ps.length == 0)
                return m.getName() + "()";
            String t = m.getName() + "(" + ps[0].getSimpleName();
            for (int i = 1; i < ps.length; i++)
                t += ", " + ps[i].getSimpleName();
            return t + ")";
        }

        public void draw(Graphics g, boolean selected) {
            g.setColor(FUNDO);
            g.fillRect(x, y, getWidth(), getHeight());

            g.setColor(Color.BLACK);
            g.drawRect(x, y + LINE_HEIGHT, getWidth(), LINE_HEIGHT * atributos.size());
            g.drawRect(x, y + LINE_HEIGHT * (1 + atributos.size()), getWidth(), LINE_HEIGHT * metodos.size());
            g.setColor(selected ? Color.RED : Color.BLACK);
            g.drawRect(x, y, getWidth(), LINE_HEIGHT);
            g.setColor(Color.BLACK);

            int h = g.getFontMetrics().getHeight();
            g.drawString(classe.getName(), x + (WIDTH - g.getFontMetrics().stringWidth(classe.getName())) / 2, y + h);
            for (int i = 0; i < atributos.size(); i++)
                g.drawString(atributos.get(i).toString(), x + 5, y + (i + 1) * LINE_HEIGHT + h);
            for (int i = 0; i < metodos.size(); i++)
                g.drawString(metodos.get(i).toString(), x + 5, y + (i + atributos.size() + 1) * LINE_HEIGHT + h);
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        public int getWidth() {
            return WIDTH;
        }

        public int getHeight() {
            return LINE_HEIGHT * (atributos.size() + metodos.size() + 1);
        }
        
        public int itemAt(int y) {
            int it = (y - LINE_HEIGHT) / LINE_HEIGHT;
            if (it < 0 || it >= atributos.size() + metodos.size())
                return -1;
            return it;
        }

        public void removeAt(int y) {
            int pos = itemAt(y);
            if (pos < atributos.size())
                atributos.remove(pos);
            else
                metodos.remove(pos - atributos.size());
        }

        private Visibilidade getVisibilidade(int m) {
            if (Modifier.isPublic(m))
                return Visibilidade.PUBLIC;
            if (Modifier.isProtected(m))
                return Visibilidade.PROTECTED;
            if (Modifier.isPrivate(m))
                return Visibilidade.PRIVATE;
            return Visibilidade.PACKAGE;
        }

        private class Membro implements Serializable {
            private static final long serialVersionUID = 1L;

            private String nome;
            private Class tipo;
            private Visibilidade visibilidade;

            public Membro(String nome, Class tipo, Visibilidade visibilidade) {
                this.nome = nome;
                this.tipo = tipo;
                this.visibilidade = visibilidade;
            }

            public boolean is(Class t) {
                return tipo.equals(t);
            }

            @Override
            public String toString() {
                return visibilidade.p() + " " + nome;
            }
        }
    }

    private enum Visibilidade {

        PUBLIC('+'), PROTECTED('#'), PACKAGE('~'), PRIVATE('-');
        private char c;

        private Visibilidade(char c) {
            this.c = c;
        }

        public char p() {
            return c;
        }
    }

    private class Desenho extends JPanel {

        public Desenho() {
            this.addMouseListener(new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent e) {
                    if (e.getButton() == MouseEvent.BUTTON1) {
                        if (!selectAt(e.getX(), e.getY()))
                            selection = -1;
                    } else if (e.getButton() == MouseEvent.BUTTON3) {
                        if (selection != -1) {
                            classes.get(selection).x = e.getX();
                            classes.get(selection).y = e.getY();
                        }
                    } else {
                        if (selCon == -1) {
                            //selects one
                            for (int i = 0; i < cons.size(); i++)
                                if (cons.get(i).contains(e.getX(), e.getY(), classes)) {
                                    System.out.println("sel" + i);
                                    selCon = i;
                                    repaint();
                                    return;
                                }
                            
                            //none found
                            //start a new conexao
                            for (int i = 0; i < classes.size(); i++)
                                if (e.getX() > classes.get(i).x && e.getX() < classes.get(i).x + Classe.WIDTH) {
                                    int it = classes.get(i).itemAt(e.getY() - classes.get(i).y);
                                    if (it != -1) {
                                        if (iniMem == -1) {
                                            selection = i;
                                            iniMem = it;
                                            iniBeg = e.getX() - classes.get(i).x < Classe.WIDTH/2;
                                            break;
                                        } else {
                                            cons.add(new Conexao(selection, iniMem, iniBeg, i, it, e.getX() - classes.get(i).x < Classe.WIDTH/2));
                                            iniMem = -1;
                                        }
                                    }
                                }
                        } else {
                            cons.get(selCon).addPoint(new Point.Inteiro(e.getX(), e.getY()));
                        }
                    }
                    repaint();
                }
            });
        }

        @Override
        public void paintComponent(Graphics g) {
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, getWidth(), getHeight());
            
            for (int i = 0; i < cons.size(); i++)
                cons.get(i).draw(g, classes, selCon == i);

            for (int i = 0; i < classes.size(); i++)
                classes.get(i).draw(g, i == selection);
        }
    }
}
